﻿/*
   Open Intel 
   Copyright © 2011 – ISC. All Rights Reserved.
*/

using System;
using System.Collections.Generic;
using System.ComponentModel.Composition;
using System.Linq;
using System.Windows.Media;
using System.Windows.Threading;
using Caliburn.Micro;
using ISC.MapDotNetServer.Common;
using ISC.MapDotNetServer.Common.Maps;
using ISC.RIM.Silverlight;
using ISC.RIM.Silverlight.MapViewElement;
using ISC.RIM.Silverlight.Render;
using ISC.RIM.Silverlight.Requestor;
using OI.Controls;
using OI.DataModel;
using OI.Events;
using OI.Framework;
using OI.Views;
using Action = System.Action;
using Envelope = ISC.RIM.Silverlight.Envelope;
using Map = ISC.RIM.Silverlight.Map;
using Point = System.Windows.Point;

namespace OI.ViewModels
{
    [Export(typeof(MashupMapViewModel))]
    [PartCreationPolicy(CreationPolicy.Shared)]
    public class MashupMapViewModel : Screen , 
        IHandle<ChangeLayerVisibility>,
        IHandle<ChangeLayerFilter>,
        IHandle<AppendLayerFilter>,
        IHandle<ChangeLayerDensityMapEnabled>,
        IHandle<ShowShape>,
        IHandle<ClearShape>,
        IHandle<ClearAllShapes>,
        IHandle<ZoomToShape>,
        IHandle<MapLayerUpInList>,
        IHandle<MapLayerDownInList>,
        IHandle<MapLayerRemoveFromList>,
        IHandle<SaveState>,
        IHandle<RestoreState>,
        ICommandTarget
    {
        #region DATA
        readonly IEventAggregator Events;
        public bool IsMapInitialized { get; private set; }
        public Map RimMap { get; private set; }
        public TileLayer BaseMapTileLayer { get; private set; }
        public TileLayer DensityMapTileLayer { get; private set; }
        public TileLayer DynamicLabelingTileLayer { get; private set; }
        public TileLayer MdnTileLayer { get; private set; }
        public Action MapRestoreAction { get; private set; }
        public AppendLayerFilter AppendLayerFilter { get; private set; }
        public Guid ShowShapeOneShotID { get; private set; }

        // holds the master mash-up map definition
        public MashupMapDefinition MashupMapDefinition { get; private set; }

        // holds the list of layers to display by ID
        public IList<string> LayerIDs
        {
            get
            {
                return _LayerIDs;
            }
            set
            {
                _LayerIDs = value;
                if (IsMapInitialized)
                {
                    if (SetDescriptor())
                    {
                        MdnTileLayer.NotifyDescriptorChange();
                    }

                    MapReadyNotifications();
                }
            }
        }
        private IList<string> _LayerIDs;


        // this is the descriptor on the MDN UX TileLayer on the map
        // it defines all of the spatial layers displayed
        public MapRequest MdnMapRequest
        {
            get
            {
                if (_MdnMapRequest == null && 
                    MdnTileLayer != null &&
                    MdnTileLayer.Descriptor is MapRequest)
                {
                    _MdnMapRequest = (MapRequest)(MdnTileLayer.Descriptor);
                }
                return _MdnMapRequest;
            }
            private set
            {
                _MdnMapRequest = value;
                MdnTileLayer.Descriptor = _MdnMapRequest;
            }
        }
        private MapRequest _MdnMapRequest;


        // keep track of shapes (drawn or highlight)
        readonly Dictionary<Guid, Tuple<IMapViewChild, ShowShape>> ShapeCache =
            new Dictionary<Guid, Tuple<IMapViewChild, ShowShape>>();


        /// <summary>
        /// Timer used to automatically refresh geom layer for density mapping
        /// </summary>
        /// <value>The slider change timer.</value>
        public DispatcherTimer GeomLayerRefreshTimer
        {
            get
            {
                if (_GeomLayerRefreshTimer == null)
                {
                    _GeomLayerRefreshTimer = new DispatcherTimer {Interval = TimeSpan.FromMilliseconds(150)};
                    _GeomLayerRefreshTimer.Tick += (s, e) => DmRefreshCheck();
                }
                return _GeomLayerRefreshTimer;
            }
        }
        private DispatcherTimer _GeomLayerRefreshTimer;

        #endregion

        #region CMicro
        // always construct from an array of map IDs
        [ImportingConstructor]
        public MashupMapViewModel(IEventAggregator events, MashupMapDefinition mmd)
        {
            Events = events;
            Events.Subscribe(this);
            MashupMapDefinition = mmd;
        }


        // early in the lifecycle provides opportunity to tap into the map initialization process
        // we need to dynamically add an MDNS map TileLayer based on the map ID defined in DatasetDetail
        protected override void OnViewAttached(object view, object context)
        {
            base.OnViewAttached(view, context);

            if (!IsMapInitialized)
            {
                // get Map from view (layout is not in place yet)
                RimMap = ((MashupMapView)view).myMap;
                RimMap.InitializeMVCComplete += InitializeMvcComplete;
                RimMap.PostInitializePosition += PostInitializePosition;
                RimMap.ExceptionEvent += OnError;

                // set the MDN services endpoint
                RimMap.MDNSAddress = Configuration.GetBaseURI() + Configuration.MDNSEndpoint;

                // Bing
                BaseMapTileLayer = ((MashupMapView)view).BaseMapTileLayer;
                var tr = (BingMapsTileRequestor)(BaseMapTileLayer.TileRequestor);
                tr.EndpointAddress = "http://ecn.t{1}.tiles.virtualearth.net/tiles/{0}{2}.{3}?g={4}&mkt=en-us&lbl=l1&stl=h&shading=hill&n=z";
                tr.Generation = Configuration.BingMapsGeneration;

                // density map TileLayer
                DensityMapTileLayer = ((MashupMapView)view).DensityMapTileLayer;
                DensityMapTileLayer.Descriptor = Configuration.Jurisdiction.MashupMapID;
                DensityMapTileLayer.TileRenderer = new DensityMapRenderer();

                // dynamic labeling TileLayer
                DynamicLabelingTileLayer = ((MashupMapView)view).LabelTileLayer;
                DynamicLabelingTileLayer.Descriptor = Configuration.Jurisdiction.MashupMapID;
                
                // MDNS TileLayer uses a blank mashup map
                MdnTileLayer = ((MashupMapView)view).MDNTileLayer;
                SetDescriptor();

                // setup routed event bindings
                var binding = new CommandBinding(DragElement.EnvelopeCompleteCommand);
                binding.Executed += DragElementCompleted;
                CommandBindings.Add(binding);
            }
        }


        // shut off drill-down when leaving
        protected override void OnDeactivate(bool close)
        {
            base.OnDeactivate(close);
            Events.Publish(new EnableDrillDown
            {
                Enable = false
            });
        }
        #endregion

        #region MAP SETUP
        // build a new descriptor for the MDNS Map TileLayer
        private bool SetDescriptor()
        {
            bool configChange = false;

            // create a master list and the actual mashup
            var mashupMasterMap = MashupMapDefinition.Copy();

            // if this is the first pass, create a new MapRequest descriptor with no layers
            // otherwise we are using and existing descriptor
            if (MdnMapRequest == null)
            {
                var blankMap = MashupMapDefinition.Copy();
                blankMap.Layers.Clear();
                MdnMapRequest = new MapRequest { ImageType = MapRequest.SupportedImageTypes.PNG8, Map = blankMap };
            }

            // get the existing layers in the map
            var existingLayers = MdnMapRequest.Map.Layers;
            var existingLayerIDs = existingLayers.Select(l => l.ID);
            
            // reconcile the layers from the mashup master list with the layer ids expected in this mashup
            // compare against the current mashup list
            foreach (string layerID in LayerIDs)
            {
                // if our current map layer list does not have the layer, add it
                var layerIDs = existingLayerIDs as List<string> ?? existingLayerIDs.ToList();
                if (!layerIDs.Contains(layerID))
                {
                    var newLayer = mashupMasterMap.GetLayerByID(layerID);
                    if (newLayer != null)
                    {
                        newLayer.Visible = true;
                        existingLayers.Add(newLayer);
                        configChange = true;
                    }
                }
            }

            // remove unused layers
            for (int i = 0; i < existingLayers.Count; i++)
            {
                var existingLayer = existingLayers[i];
                if (!LayerIDs.Contains(existingLayer.ID))
                {
                    existingLayers.Remove(existingLayer);
                    configChange = true;
                }
            }

            return configChange;
        }


        // called by RIM when the map MVC model is initialized
        public void InitializeMvcComplete(object sender, EventArgs e)
        {
            RimMap.MapController.ForwardOnlyUnhandledMouseGestures = true;
            RimMap.MapView.EnableMouseHoverEvents = MdnMapRequest.Map.AreMouseHoverEventsEnabled();

            // route all commands to this
            RimMap.RoutedCommandTarget = this;

            IsMapInitialized = true;
        }


        // called by RIM when map position is initialized
        public void PostInitializePosition(object sender, EventArgs e)
        {
            // restore map state action if provided
            if (MapRestoreAction != null)
            {
                MapRestoreAction();
                MapRestoreAction = null;
            }

            // if we are to append a filter to a layer
            // grab existing filters (if any), disable them, and append the new one
            if (AppendLayerFilter != null)
            {
                string id = AppendLayerFilter.LayerID;
                var layer = MdnMapRequest.Map.GetLayerByID(id);
                if (layer != null)
                {
                    var fis = FilterItem.ParseFilterItems(layer);
                    fis.Apply(i => i.IsEnabled = false);
                    fis.Add(AppendLayerFilter.Item);
                    FilterItem.ApplyFilterToLayer(fis, layer);
                }
            }

            // send out relevent notifications since the map is ready
            MapReadyNotifications();
        }


        // now that everything is set up, notify the rest of the application the map is ready
        private void MapReadyNotifications()
        {
            // set out an event that a new Map has been configured and is being displayed
            // publish event
            if (MdnMapRequest != null)
            {
                Events.Publish(new SelectMap
                {
                    Map = MdnMapRequest.Map
                });
            }

            // send out an initial scale event so the legend can get started
            Events.Publish(new ChangeMapScale
            {
                Scale = RimMap.MapView.GetMapPyramidScale(),
                ZoomLevel = RimMap.ZoomLevel
            });

            // we now have a fully configured RIM map displayed
            // send out a notification
            Events.Publish(new DisplayRIMMap
            {
                Map = RimMap,
                BaseTileLayer = BaseMapTileLayer,
                MDNTileLayer = MdnTileLayer
            });

            // start out in drill-down mode
            Events.Publish(new ChooseToolboxItem
            {
                Group = "QueryToolboxItems",
                Key = "QueryDrillDown"
            });
        }


        // RIM exceptions route here, forward onto shell
        public void OnError(object sender, MapControlExceptionEventArgs arg)
        {
            IoC.Get<IShell>().HandleException(arg.Exception, "MDN UX map error:");
        }


        // called when map extents changes for any reason
        public void ViewChangeComplete()
        {
            double scale = RimMap.MapView.GetMapPyramidScale();

            // publish scale change event
            Events.Publish(new ChangeMapScale
            {
                Scale = scale,
                ZoomLevel = RimMap.ZoomLevel
            });

            // force refresh of any density mapping
            if (OldScale != scale)
            {
                DmForceRefresh();
            }

            OldScale = scale;
        }
        private double OldScale { get; set; }


        // mouse hover on the map
        public void MouseHover(PointPairEventArgs parm)
        {
            Events.Publish(new MouseHoverOverMap
            {
                Location = parm
            });
        }


        // mouse left click on map
        public void MouseLeftSingleClickPosition(PointPairEventArgs parm)
        {
            Events.Publish(new MouseLeftClickOnMap
            {
                Location = parm
            });
        }


        // called when a drag-element operation completes
        void DragElementCompleted(object sender, ExecutedRoutedEventArgs e)
        {
            var arg = e.Parameter as EnvelopeEventArgs;
            e.Handled = true;

            if (arg != null)
            {
                Events.Publish(new DragElementCompleted
                {
                    Rect = arg.Envelope
                });
            }
        }
        #endregion

        #region HANDLERS

        // layer visibility change on the MDN Map
        public void Handle(ChangeLayerVisibility message)
        {
            MdnTileLayer.NotifyDescriptorChange();
        }


        // layer filter change on the MDN Map
        public void Handle(ChangeLayerFilter message)
        {
            MdnTileLayer.NotifyDescriptorChange();

            // if we have a density map turned on for this layer, re-setup since the filter changed
            if (message.LegendItem.IsDensityMapVisible)
            {
                var layer = message.Layer;
                DensityMapSetup(layer);
                DmForceRefresh();
            }

            // if this layer has dynamic labeling
            if (message.Layer.HasDynamicLabeling())
            {
                DynamicLabelingSetup();
            }
        }


        // programatic appending of a layer filter
        // this is added to existing layer filter at start-up
        public void Handle(AppendLayerFilter message)
        {
            AppendLayerFilter = message;
        }


        // show an IMapViewChild on map - supports elements or shapes
        public void Handle(ShowShape message)
        {
            if (RimMap != null &&
                RimMap.MapController != null)
            {
                IMapViewChild child = null;

                // clear any one-shots
                if (ShowShapeOneShotID != Guid.Empty)
                {
                    Handle(new ClearShape 
                    {
                        ID = ShowShapeOneShotID
                    });
                    ShowShapeOneShotID = Guid.Empty;
                }

                // if this a one-shot
                if (message.OneShot)
                {
                    ShowShapeOneShotID = message.ID;
                }

                // if colors not provided in message, use current
                if (string.IsNullOrEmpty(message.Fill))
                {
                    message.Fill = MapShapes.FillBrush.Color.ToString();
                }
                if (string.IsNullOrEmpty(message.Outline))
                {
                    message.Outline = MapShapes.OutlineBrush.Color.ToString();
                }

                // drawing text
                if (message.ShowShapeType == ShowShape.ShowShapeTypes.DrawnText)
                {
                    // this post-sets actual text and size after the user commits their entry
                    Action<string, double> textSetAction = (text, size) =>
                    {
                        message.Text = text;
                        message.TextSize = size;
                    };

                    TextMarker tm;
                    if (string.IsNullOrEmpty(message.Text))
                    {
                        tm = new TextMarker(
                            MapShapes.OutlineBrush, 
                            textSetAction);
                    }
                    else
                    {
                        tm = new TextMarker(
                            message.Text, 
                            message.TextSize, 
                            new SolidColorBrush(Framework.Utility.GetColorFromHex(message.Outline)));
                    }
                    child = new PointElement(new Point(message.LocationX, message.LocationY), tm);
                }

                // drawing shapes
                else if (!string.IsNullOrEmpty(message.ShapeSerial) && 
                    message.ShowShapeType == ShowShape.ShowShapeTypes.Highlight)

                {
                    child = MapShapes.CreateHighlightShape(Shape.DeserializeShape(message.ShapeSerial));
                }

                else if (!string.IsNullOrEmpty(message.ShapeSerial) && 
                    message.ShowShapeType == ShowShape.ShowShapeTypes.DrawnShape)
                {
                     child = MapShapes.CreateDrawnShape(Shape.DeserializeShape(message.ShapeSerial), 
                                new SolidColorBrush(Framework.Utility.GetColorFromHex(message.Fill)),
                                new SolidColorBrush(Framework.Utility.GetColorFromHex(message.Outline)));
                }

                // drawing marker point
                else if (message.ShowShapeType == ShowShape.ShowShapeTypes.DrawnPoint)
                {
                    child = MapShapes.CreateDrawPoint(
                        new Point(message.LocationX, message.LocationY),
                        new SolidColorBrush(Framework.Utility.GetColorFromHex(message.Fill)),
                        new SolidColorBrush(Framework.Utility.GetColorFromHex(message.Outline)));
                }

                // add shape to map and to cache along with the ShowShape message as metadata for sharing purposes
                if (child != null)
                {
                    var cacheItem = new Tuple<IMapViewChild, ShowShape>(child, message);
                    ShapeCache.Add(message.ID, cacheItem);
                    RimMap.MapController.AddMapViewChild(child);
                }
            }
        }


        // clear an IMapViewChild by ID
        public void Handle(ClearShape message)
        {
            Guid key = message.ID;

            if (RimMap != null &&
                RimMap.MapController != null &&
                ShapeCache.ContainsKey(key))
            {
                var shape = ShapeCache[key];
                RimMap.MapController.RemoveMapViewChild(shape.Item1);
                ShapeCache.Remove(key);
            }
        }


        // clear all overlay shapes
        public void Handle(ClearAllShapes message)
        {
            if (RimMap != null &&
                RimMap.MapController != null &&
                RimMap.MapController.DrawingMode == MapController.DrawingModes.None)
            {
                RimMap.MapView.ContentElementOverlay.Clear(null);
                ShapeCache.Clear();
            }
        }


        // zoom to a shape on map
        public void Handle(ZoomToShape message)
        {
            var shape = message.Shape;
            
            if (shape is ISC.MapDotNetServer.Common.Point)
            {
                var pt = shape.Bounds.Center.ToSysWinPoint();
                RimMap.MapController.FlyToAnimate(pt, Configuration.MaxZoomToShapeZoomLevel, null, null); 
            }
            else
            {
                RimMap.MapController.FlyToAnimate(Envelope.Create(shape.Bounds), null, null);
            }
        }


        // update map layer order (remember layers are in reverse order to legend)
        public void Handle(MapLayerUpInList message)
        {
            if (MdnMapRequest != null && RimMap != null)
            {
                var map = MdnMapRequest.Map;
                var layer = message.Layer;

                int idx = map.Layers.IndexOf(layer);
                if (idx > -1 && idx < map.Layers.Count - 1)
                {
                    map.Layers.Remove(layer);
                    map.Layers.Insert(idx + 1, layer);
                    MdnTileLayer.NotifyDescriptorChange();
                }
            }
        }


        // update map layer order (remember layers are in reverse order to legend)
        public void Handle(MapLayerDownInList message)
        {
            // update map (remember layers are in reverse order to legend)
            if (MdnMapRequest != null && RimMap != null)
            {
                var map = MdnMapRequest.Map;
                var layer = message.Layer;

                int idx = map.Layers.IndexOf(layer);
                if (idx > 0)
                {
                    map.Layers.Remove(layer);
                    map.Layers.Insert(idx - 1, layer);
                    MdnTileLayer.NotifyDescriptorChange();
                }
            }
        }


        // remove an MDN layer from the map layers
        public void Handle(MapLayerRemoveFromList message)
        {
            if (MdnTileLayer != null && MdnMapRequest != null)
            {
                MdnMapRequest.Map.Layers.Remove(message.Layer);
                MdnTileLayer.NotifyDescriptorChange();
            }
        }

        // turn density mapping on/off
        public void Handle(ChangeLayerDensityMapEnabled message)
        {
            // start/stop geometry layer refresh timer depending if we are showing a hotspot layer
            if (message.IsEnabled)
            {
                DensityMapSetup(message.Layer);
                GeomLayerRefreshTimer.Start();
                DensityMapTileLayer.IsVisible = true;
                DmForceRefresh();
            }
            else
            {
                GeomLayerRefreshTimer.Stop();
                DensityMapTileLayer.IsVisible = false;
            }
        }

        #endregion

        #region DENSITY MAPPING

        public void DensityMapSetup(Layer layer)
        {
            // DM metadata
            var dmItem = DensityMapItem.ParseDensityMapItem(layer);

            // update descriptor to point to correct layer selected
            var requestor = DensityMapTileLayer.TileRequestor as MDNSGeomTileRequestor;
            if (requestor != null && RimMap != null)
            {
                requestor.LayerID = layer.ID;

                // make sure min/max scale matches with layer
                DensityMapTileLayer.MaxScaleDenom = layer.GetMaxScale();
                DensityMapTileLayer.MinScaleDenom = layer.GetMinScale();

                // make the feature service where clause match the layer's
                requestor.Filter = layer.ConnectionSpecification.WhereClause;

                // build the correct layer descriptor and add the column defined in metadata
                var descriptor = requestor.BuildDescriptor(Configuration.Jurisdiction.MashupMapID, RimMap.TileProjection);
                if (!string.IsNullOrEmpty(dmItem.ColumnName))
                {
                    descriptor.QueryRequest.Queries[0].Fields.Add(dmItem.ColumnName);
                }

                DensityMapTileLayer.Descriptor = descriptor;
            }

            // update renderer to use appropriate computational method
            var dmRenderer = (DensityMapRenderer)(DensityMapTileLayer.TileRenderer);
            if (dmItem.Operation.ToLower() == "count")
            {
                dmRenderer.AccumType = DensityMapRenderer.AccumTypes.Count;
            }
            else if (dmItem.Operation.ToLower() == "sum")
            {
                dmRenderer.AccumType = DensityMapRenderer.AccumTypes.Sum;
            }
        }


        public void DmRefreshCheck()
        {
            if (RimMap != null &&
                DensityMapTileLayer != null)
            {
                var dmRenderer = (DensityMapRenderer)(DensityMapTileLayer.TileRenderer);

                if (_ForceRefreshOnNextCheck)
                {
                    dmRenderer.MaxValue = 0;
                    _DmMaxValue = -1;
                    _ForceRefreshOnNextCheck = false;
                }

                if (dmRenderer.MaxValue > _DmMaxValue)
                {
                    _DmMaxValue = dmRenderer.MaxValue;
                    RimMap.MapController.RefreshSingleTileLayer("DensityMapTileLayer", false, true);
                }
            }
        }


        public void DmForceRefresh()
        {
            _ForceRefreshOnNextCheck = true;
        }
        private double _DmMaxValue = -1;
        private bool _ForceRefreshOnNextCheck;
        #endregion

        #region DYNAMIC LABELING
        // finds the first layer with dynamic labeling and sets it up
        public void DynamicLabelingSetup()
        {
            // search for an appropriate layer
            Layer layer = MdnMapRequest.Map.Layers.FirstOrDefault(l => l.HasDynamicLabeling());

            if (layer != null)
            {
                // DL metadata
                var dlItem = DynamicLabelingItem.ParseDynamicLabelingItem(layer);

                // update descriptor to point to correct layer selected
                var requestor = DynamicLabelingTileLayer.TileRequestor as MDNSGeomTileRequestor;
                if (requestor != null && RimMap != null)
                {
                    // make the feature service where clause match the layer's
                    requestor.Filter = layer.ConnectionSpecification.WhereClause;

                    // set layer id
                    requestor.LayerID = layer.ID;

                    // set min zoom level
                    DynamicLabelingTileLayer.MinScaleDenom = dlItem.MinZoomLevel;

                    // add renderer
                    DynamicLabelingTileLayer.TileRenderer = new DynamicLabelRenderer(
                           RimMap,
                           () => RimMap.MapController.RefreshSingleTileLayer("LabelTileLayer", false, true),
                           0,
                           1);

                    // build the correct layer descriptor and add the column defined in metadata
                    var descriptor = requestor.BuildDescriptor(Configuration.Jurisdiction.MashupMapID, RimMap.TileProjection);
                    descriptor.QueryRequest.Queries[0].Fields.Add(dlItem.PrimaryKeyColName);
                    descriptor.QueryRequest.Queries[0].Fields.Add(dlItem.LabelTextColName);
                    DynamicLabelingTileLayer.Descriptor = descriptor;

                    // make TileLayer visible
                    DynamicLabelingTileLayer.IsVisible = true;
                }
            }
        }

        #endregion

        #region SHARE
        // request to save the current state of the map
        public void Handle(SaveState message)
        {
            if (RimMap != null &&
                MdnMapRequest != null)
            {
                // layers in our mashup
                var layers = MdnMapRequest.Map.Layers;

                var mapState = new MapState
                {
                    BaseMapDescriptor = BaseMapTileLayer.Descriptor as string,
                    ZoomLevel = RimMap.ZoomLevel,
                    MapCenterSerial = RimMap.MapCenter.ToMDNSPoint().Serialize(),
                    LayerIDs = (from l in layers select l.ID).ToList(),
                    VisibilityStates = (from l in layers select l.Visible).ToList(),
                    LayerFilterMetadata = layers.Select(l => l.MetaData.ContainsKey(Configuration.FiltersMetaKey) ? 
                        l.MetaData[Configuration.FiltersMetaKey] : "").ToList(),
                    DensityMapMetadata = layers.Select(l => l.GetDensityMappingString()).ToList(),
                    ShapeMetadata = ShapeCache.Select(i => i.Value.Item2).ToList(),
                };

                // json serialize
                string stateStr = MessageSerializer.Serialize(mapState);

                // write to state
                message.Write(GetType().ToString(), stateStr);
            }
        }


        // request to restore the current state of the map
        public void Handle(RestoreState message)
        {
            string stateStr = message.Read(GetType().ToString());
            var mapState = MessageSerializer.Deserialize<MapState>(stateStr);

            var basemapDescriptor = mapState.BaseMapDescriptor;
            var zl = mapState.ZoomLevel;
            var mapCenterStr = mapState.MapCenterSerial;
            var mapCenter = ISC.MapDotNetServer.Common.Point.Deserialize(mapCenterStr);
            var layerIDs = mapState.LayerIDs;
            var visibilityStates = mapState.VisibilityStates;
            var layerFilterMetadata = mapState.LayerFilterMetadata;
            var densityMapMetadata = mapState.DensityMapMetadata;
            var shapeMetadata = mapState.ShapeMetadata;

            MapRestoreAction = () =>
            {
                int idx = 0, idx2 = 0, idx3 = 0;

                // restore base map descriptor (road/satellite) 
                BaseMapTileLayer.Descriptor = basemapDescriptor;

                // re-order map layers according to the saved order list
                // also set their visibility states
                var map = MdnMapRequest.Map;
                var oldLayers = map.Layers;
                var newLayers = layerIDs.Select(id => oldLayers.First(l => l.ID == id)).ToList();
                newLayers.Apply(l => l.Visible = visibilityStates[idx++]);
                map.Layers = newLayers;

                // update layer filter metadata
                if (layerFilterMetadata != null && layerFilterMetadata.Count > 0)
                {
                    foreach (var l in newLayers)
                    {
                        l.MetaData[Configuration.FiltersMetaKey] = layerFilterMetadata[idx2++];
                    }
                }

                // update density metadata
                if (densityMapMetadata != null && densityMapMetadata.Count > 0)
                {
                    foreach (var l in newLayers)
                    {
                        l.SetDensityMappingString(densityMapMetadata[idx3++]);
                    }
                }

                // update map
                MdnTileLayer.NotifyDescriptorChange();

                // replay shape metadata to rebuild shapes
                foreach(var showShape in shapeMetadata)
                {
                    Events.Publish(showShape);
                }

                RimMap.MapController.FlyToAnimate(mapCenter.ToSysWinPoint(), zl, null, EasingValues.FlyToInternal);
            };
        }

        public class MapState
        {
            public string BaseMapDescriptor { get; set; }
            public double ZoomLevel { get; set; }
            public string MapCenterSerial { get; set; }
            public List<string> LayerIDs { get; set; }
            public List<bool> VisibilityStates { get; set; }
            public List<string> LayerFilterMetadata { get; set; }
            public List<string> DensityMapMetadata { get; set; }
            public List<ShowShape> ShapeMetadata { get; set; }
            public string LastQueryShapeSerial { get; set; }
        }
        #endregion

        #region NAVIGATOR
        // Handles ModeChanged event of the navigator control for selecting base layer type.
        public void NavigatorModeChanged(NavModeEventArgs e)
        {
            if (BaseMapTileLayer != null)
            {
                BaseMapTileLayer.Descriptor = e.Mode;
            }
        }


        // Handles the Zoomed event of the navigator control.
        public void NavigatorZoomed(NavZoomEventArgs e)
        {
            if (RimMap.IsMVCInitialized)
            {
                double z = RimMap.ZoomLevel + e.ZoomChange;
                if (z > 20) { z = 20; } else if (z < 1) { z = 1; }
                if (z != RimMap.ZoomLevel)
                {
                    RimMap.ZoomLevelAnimate = z;
                }
            }
        }


        // Handles the Panned event of the navigator control.
        public void NavigatorPanned(NavPanEventArgs e)
        {
            if (RimMap.IsMVCInitialized)
            {
                Envelope curExt = RimMap.MapView.GetMapExtents();
                double magX = curExt.MagX / 10.0;
                double magY = curExt.MagY / 10.0;
                ISC.MapDotNetServer.Common.Point newCenter = curExt.Center.ToMDNSPoint().Offset(magX * e.PanVector.X, magY * e.PanVector.Y);
                Execute.OnUIThread(() => RimMap.MapCenterAnimate = newCenter.ToSysWinPoint());
            }
        }

        #endregion

        #region ICommandTarget Members

        /// <summary>
        /// Gets the command bindings.
        /// </summary>
        public List<CommandBinding> CommandBindings
        {
            get { return _CommandBindings ?? (_CommandBindings = new List<CommandBinding>()); }
        }
        private List<CommandBinding> _CommandBindings;

        /// <summary>
        /// Gets the routing parent.
        /// </summary>
        public ICommandTarget RoutingParent
        {
            get { return null; }
        }

        #endregion
    }
}
